package com.qcrud;

import com.qcrud.core.*;
import com.qcrud.core.paging.DialectFactory;
import com.qcrud.core.paging.Paging;
import com.qcrud.core.parsing.MdSqlParser;
import com.qcrud.core.result.QueryResult;
import com.qcrud.core.spi.QcrudPlugin;
import com.qcrud.core.transaction.TransactionIsolationLevel;
import com.qcrud.core.type.FieldTypeHandler;
import com.qcrud.core.type.TypeHandler;
import com.qcrud.exception.QcrudException;
import com.qcrud.exception.Unchecked;
import com.qcrud.jte.JteSqlTemplate;
import com.qcrud.mapper.MapperProxyFactory;
import lombok.extern.slf4j.Slf4j;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

@Slf4j
public class Qcrud {
    private final CopyOnWriteArrayList<QcrudPlugin> plugins = new CopyOnWriteArrayList<>();
    private final Map<Class<?>, Object> mapperCache = new ConcurrentHashMap<>();
    private final ConnectionFactory connectionFactory;
    private SqlTemplate sqlTemplate = new JteSqlTemplate();
    private List<SqlHandler> sqlHandlerList;

    private Qcrud(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    public static Qcrud create(DataSource dataSource) {
        return create(dataSource, null);
    }

    public static Qcrud create(DataSource dataSource, String filename) {
        try {
            return create(dataSource.getConnection(), filename);
        } catch (SQLException e) {
            throw new QcrudException("get connection error", e);
        }
    }

    public static Qcrud create(Connection connection) {
        return create(connection, null);
    }

    public static Qcrud create(Connection connection, String filename) {
        Qcrud qcrud = new Qcrud(new SingleConnectionFactory(connection));
        if (null != filename) {
            try {
                // 解析 Mapper md 文件
                MdSqlParser.parse(filename);
            } catch (IOException e) {
                throw new QcrudException("Failed to parse mapper.md file");
            }
        }
        return qcrud;
    }

    public Qcrud setSqlTemplate(SqlTemplate sqlTemplate) {
        this.sqlTemplate = sqlTemplate;
        return this;
    }

    public SqlTemplate getSqlTemplate() {
        if (null == this.sqlTemplate) {
            throw new QcrudException("sqlTemplate Cannot be empty");
        }
        return this.sqlTemplate;
    }

    public Qcrud addSqlHandler(SqlHandler sqlHandler) {
        if (null == sqlHandlerList) {
            sqlHandlerList = new ArrayList<>();
        }
        sqlHandlerList.add(sqlHandler);
        return this;
    }

    /**
     * 添加返回字段类型处理器
     *
     * @param clazz       字段类型
     * @param typeHandler 类型处理器
     * @return
     */
    public Qcrud addTypeHandler(Class<?> clazz, TypeHandler typeHandler) {
        FieldTypeHandler.put(clazz, typeHandler);
        return this;
    }

    public Connection getConnection() {
        return connectionFactory.openConnection();
    }

    public static Qcrud create(final String url, final String username, final String password) {
        Objects.requireNonNull(url, "null url");
        Objects.requireNonNull(username, "null username");
        Objects.requireNonNull(password, "null password");
        try {
            return create(DriverManager.getConnection(url, username, password));
        } catch (SQLException e) {
            throw new QcrudException("get connection error", e);
        }
    }

    public Qcrud installPlugins() {
        ServiceLoader.load(QcrudPlugin.class).forEach(this::installPlugin);
        log.debug("Automatically installed plugins {}", plugins);
        return this;
    }

    public Qcrud installPlugin(QcrudPlugin plugin) {
        if (plugins.addIfAbsent(plugin)) {
            Unchecked.consumer(plugin::customizeQcrud).accept(this);
        }
        return this;
    }

    public Handle open() {
        try {
            final long start = System.nanoTime();
            Connection conn = this.getConnection();
            final long stop = System.nanoTime();
            for (QcrudPlugin p : plugins) {
                conn = p.customizeConnection(conn);
            }
            Handle handle = new Handle(conn);
            for (QcrudPlugin p : plugins) {
                handle = p.customizeHandle(handle);
            }
            if (log.isDebugEnabled()) {
                log.debug("Qcrud [{}] obtain handle [{}] in {}ms", this, handle, MILLISECONDS.convert(stop - start, NANOSECONDS));
            }
            return handle;
        } catch (SQLException e) {
            throw new QcrudException(e);
        }
    }

    public Object execute(SqlData sqlData) {
        try {
            // 开启处理器
            Handle handle = this.open();
            sqlData.setCtx(handle.getStatementContext());
            // SQL 模板引擎渲染
            if (null != sqlData.getParams()) {
                this.getSqlTemplate().render(sqlData);
            }
            // SQL 执行前处理
            if (null != sqlHandlerList) {
                for (SqlHandler sqlHandler : sqlHandlerList) {
                    sqlHandler.process(sqlData);
                }
            }
            SqlMeta sqlMeta = sqlData.getSqlMeta();
            SqlCommandType sqlCommandType = sqlMeta.getSqlCommandType();
            if (sqlCommandType == SqlCommandType.SELECT) {
                // 分页逻辑处理
                Paging paging = sqlData.getPaging();
                if (null != paging) {
                    // 执行 COUNT 查询
                    if (paging.searchCount()) {
                        String countSql = "select count(*) from (" + sqlData.getSql() + ") t";
                        QueryResult queryResult = handle.createQuery(countSql, sqlData.getArgs());
                        Object result = queryResult.mapTo(sqlData, Long.class);
                        paging.setTotal((Long) result);
                    }

                    // 分页 SQL 构建
                    DialectFactory.buildSql(sqlData);
                }
                // 执行查询
                QueryResult queryResult = handle.createQuery(sqlData.getSql(), sqlData.getArgs());
                Object result = queryResult.mapTo(sqlData, sqlMeta.getReturnType());
                if (null == paging) {
                    // 非分页查询返回普通查询结果
                    return result;
                }
                // 设置分页查询记录列表
                paging.setRecords((List) result);
                return paging;
            }
            if (sqlCommandType == SqlCommandType.INSERT || sqlCommandType == SqlCommandType.DELETE
                || sqlCommandType == SqlCommandType.UPDATE) {
                // 执行插入或更新
                return handle.createUpdate(sqlData.getSql(), sqlData.getArgs());
            }
        } catch (SQLException e) {
            throw new QcrudException(e);
        }
        return null;
    }

    public <T> T getMapper(Class<T> type) {
        T mapper = (T) mapperCache.get(type);
        if (null == mapper) {
            mapper = new MapperProxyFactory<T>(type).newInstance(this);
            mapperCache.put(type, mapper);
        }
        return mapper;
    }

    public <R, X extends Exception> R withHandle(HandleCallback<R, X> callback) throws X {
        return callback.withHandle(this.open());
    }

    public <X extends Exception> void useHandle(final HandleConsumer<X> consumer) throws X {
        withHandle(consumer.asCallback());
    }

    public <R, X extends Exception> R inTransaction(final HandleCallback<R, X> callback) throws X {
        return this.open().inTransaction(callback);
    }

    public <X extends Exception> void useTransaction(final HandleConsumer<X> consumer) throws X {
        inTransaction(consumer.asCallback());
    }

    public <R, X extends Exception> R inTransaction(TransactionIsolationLevel level, HandleCallback<R, X> callback) throws X {
        return this.open().inTransaction(level, callback);
    }

    public <X extends Exception> void useTransaction(TransactionIsolationLevel level, HandleConsumer<X> consumer) throws X {
        inTransaction(level, consumer.asCallback());
    }
}
